/*
 * Copyright 2020-2099 sa-token.cc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.dev33.satoken.dao;

import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.dao.entity.SaTokenData;
import cn.dev33.satoken.dao.mapper.SaTokenDataMapper;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Sa-Token 持久层实现 [ 数据库存储、FastJson2序列化 ]
 *
 * @author moon69
 * @since 1.37.0
 */
@Component
public class SaTokenDaoJdbcImpl implements SaTokenDao {

    private SaTokenDataMapper saTokenDataMapper;

    public SaTokenDaoJdbcImpl(SaTokenDataMapper saTokenDataMapper) {
        this.saTokenDataMapper = saTokenDataMapper;

        // 重写 SaSession 生成策略
        SaStrategy.instance.createSession = SaSessionForJdbcCustomized::new;
    }

    // --------------------- 字符串读写 ---------------------

    @Override
    public String get(String key) {
        SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key);
        if (clearKey(saTokenData)) {
            return null;
        }

        return saTokenData.getTokenValue();
    }

    @Override
    public void set(String key, String value, long timeout) {
        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }

        SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key);
        Long expireTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000);
        // 不存在则新增，存在则更新
        if (saTokenData == null) {
            saTokenData = new SaTokenData();
            saTokenData.setTokenKey(key);
            saTokenData.setTokenValue(value);
            saTokenData.setExpireTime(expireTime);
            saTokenDataMapper.insert(saTokenData);
        } else {
            saTokenData.setTokenValue(value);
            saTokenData.setExpireTime(expireTime);
            saTokenDataMapper.updateById(saTokenData);
        }
    }

    @Override
    public void update(String key, String value) {
        SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key);
        if (getKeyTimeout(saTokenData) == SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }

        saTokenData.setTokenValue(value);
        saTokenDataMapper.updateById(saTokenData);
    }

    @Override
    public void delete(String key) {
        saTokenDataMapper.deleteByTokenKey(key);
    }

    @Override
    public long getTimeout(String key) {
        return getKeyTimeout(key);
    }

    @Override
    public void updateTimeout(String key, long timeout) {
        saTokenDataMapper.updateExpireTime(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
    }

    // --------------------- 对象读写 ---------------------

    @Override
    public Object getObject(String key) {
        SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key);
        if (clearKey(saTokenData)) {
            return null;
        }

        return JSON.parseObject(saTokenData.getTokenValue(), Object.class, JSONReader.Feature.SupportAutoType);
    }

    @Override
    public void setObject(String key, Object object, long timeout) {
        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }

        SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key);
        Long expireTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000);
        // 不存在则新增，存在则更新
        if (saTokenData == null) {
            saTokenData = new SaTokenData();
            saTokenData.setTokenKey(key);
            saTokenData.setTokenValue(JSON.toJSONString(object, JSONWriter.Feature.WriteClassName));
            saTokenData.setExpireTime(expireTime);
            saTokenDataMapper.insert(saTokenData);
        } else {
            saTokenData.setTokenValue(JSON.toJSONString(object, JSONWriter.Feature.WriteClassName));
            saTokenData.setExpireTime(expireTime);
            saTokenDataMapper.updateById(saTokenData);
        }
    }

    @Override
    public void updateObject(String key, Object object) {
        SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key);
        if (getKeyTimeout(saTokenData) == SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }

        saTokenData.setTokenValue(JSON.toJSONString(object, JSONWriter.Feature.WriteClassName));
        saTokenDataMapper.updateById(saTokenData);
    }

    @Override
    public void deleteObject(String key) {
        saTokenDataMapper.deleteByTokenKey(key);
    }

    @Override
    public long getObjectTimeout(String key) {
        return getKeyTimeout(key);
    }

    @Override
    public void updateObjectTimeout(String key, long timeout) {
        saTokenDataMapper.updateExpireTime(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
    }

    // --------------------- 会话管理 ---------------------

    @Override
    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
        List<String> keyList = saTokenDataMapper.findKeyList(prefix, keyword);
        return SaFoxUtil.searchList(keyList, start, size, sortType);
    }

    // --------------------- 数据过期 ---------------------

    /**
     * @param saTokenData saToken数据
     * @return true-数据过期被清理 false-数据存活
     */
    public boolean clearKey(SaTokenData saTokenData) {
        // 不存在则当作已被清理
        if (saTokenData == null) {
            return true;
        }

        // 删除过期数据
        Long expireTime = saTokenData.getExpireTime();
        if (expireTime != SaTokenDao.NEVER_EXPIRE && expireTime < System.currentTimeMillis()) {
            saTokenDataMapper.deleteById(saTokenData);
            return true;
        }

        return false;
    }

    /**
     * 获取指定 key 的剩余存活时间 （单位：秒）
     *
     * @param key 指定 key
     * @return 这个 key 的剩余存活时间
     */
    public long getKeyTimeout(String key) {
        SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key);
        return getKeyTimeout(saTokenData);
    }

    /**
     * 获取指定 key 的剩余存活时间 （单位：秒）
     *
     * @param saTokenData saToken数据
     * @return 这个 key 的剩余存活时间
     */
    public long getKeyTimeout(SaTokenData saTokenData) {
        // 由于数据过期检测属于惰性扫描，很可能此时这个 key 已经是过期状态了，所以这里需要先检查一下
        if (clearKey(saTokenData)) {
            return SaTokenDao.NOT_VALUE_EXPIRE;
        }

        // 如果 expire 被标注为永不过期，则返回 NEVER_EXPIRE
        if (saTokenData.getExpireTime() == SaTokenDao.NEVER_EXPIRE) {
            return SaTokenDao.NEVER_EXPIRE;
        }

        // 计算剩余时间并返回 （过期时间戳 - 当前时间戳） / 1000 转秒
        long timeout = (saTokenData.getExpireTime() - System.currentTimeMillis()) / 1000;

        // 小于零时，视为不存在
        if (timeout < 0) {
            saTokenDataMapper.deleteByTokenKey(saTokenData.getTokenKey());
            return SaTokenDao.NOT_VALUE_EXPIRE;
        }

        return timeout;
    }

    // --------------------- 定时清理过期数据 ---------------------

    /**
     * 执行数据清理的线程引用
     */
    public Thread refreshThread;

    /**
     * 是否继续执行数据清理的线程标记
     */
    public volatile boolean refreshFlag;

    /**
     * 初始化定时任务，定时清理过期数据
     */
    public void initRefreshThread() {
        // 如果开发者配置了 <=0 的值，则不启动定时清理
        if (SaManager.getConfig().getDataRefreshPeriod() <= 0) {
            return;
        }

        refreshFlag = true;
        refreshThread = new Thread(() -> {
            for (; ; ) {
                try {
                    try {
                        // 如果已经被标记为结束
                        if (!refreshFlag) {
                            return;
                        }

                        // 执行清理
                        refreshDataMap();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    // 休眠N秒
                    int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod();
                    if (dataRefreshPeriod <= 0) {
                        dataRefreshPeriod = 1;
                    }
                    Thread.sleep(dataRefreshPeriod * 1000L);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        refreshThread.start();
    }

    /**
     * 清理所有已经过期的 key
     */
    public void refreshDataMap() {
        saTokenDataMapper.deleteByExpireTime(0L, System.currentTimeMillis());
    }

    @Override
    public void init() {
        // 定时清理过期数据
        initRefreshThread();
    }

    @Override
    public void destroy() {
        // 不再定时清理过期数据
        refreshFlag = false;
    }
}
